package client

import (
	"gitee.com/windwolfs/battle/apis/jrpc"
	"github.com/gorilla/websocket"
	gonanoid "github.com/matoous/go-nanoid"
	"log"
	"sync"
)

const MaxSessionIdLength = 32

type (
	// DataPacket 收发数据接口
	DataPacket interface {
		// ReadPacket 读取客户端消息（包）
		ReadPacket() (*jrpc.Message, error)

		// SendPacket 发送客户端消息包
		SendPacket(message []byte) error
	}

	// Client
	// 客户端抽象接口
	Client interface {
		Connection
		DataPacket
		SessionId() string
	}

	// Connection
	// 管理客户端连接接口
	Connection interface {
		Replace(conn *websocket.Conn) error
		SetConnection(conn *websocket.Conn)
		Close()
		Wait()
		QuoteTotal() int
	}
)

// Session
// 简单客户端，也是服务器的Session对象
// 需要时可组合其对 新的 Client 对象进行扩充
type Session struct {
	sync.RWMutex

	//	唯一的 Session ID
	sessionId string

	//	这是一个连接，应当适配所有协议
	//	但目前只适配 Websocket
	conn        *websocket.Conn
	messageType int
	replacing   bool
	offline     chan struct{}

	//	连接的引用计数器
	//	每一次替换连接时当累加一
	//	当每一次关闭连接时则累减一
	//	当销毁连接时，要判定其是否被其它连接引用了
	//	若引用了则不销毁连接
	quoteTotal int
}

// NewSession
// 这里会创建一个新的 Session 对象
func NewSession() *Session {
	return &Session{
		sessionId:   gonanoid.MustID(MaxSessionIdLength),
		messageType: websocket.TextMessage,
		offline:     make(chan struct{}),
		quoteTotal:  0,
		replacing:   false,
	}
}

// ReadPacket
// 读取数据封包
func (p *Session) ReadPacket() (*jrpc.Message, error) {
	//	这一步会阻塞
	//	获取下游二进制数据
	//	当客户端主动断开连接时会触发 error
	dataType, data, err := p.conn.ReadMessage()

	//	即当替换连接时，丢弃所有的数据包
	if p.replacing {
		log.Printf("连接替换中...%s\n", p.sessionId)
		return nil, ErrLosePacket
	}

	//	当出发了 Error
	//	则将错误返回给上游调用者
	if err != nil {
		return nil, err
	}

	//	协商协议格式
	if dataType != p.messageType {
		p.messageType = dataType
	}

	//	反序列化数据
	return jrpc.Unmarshal(data), nil
}

// SendPacket
// 发送数据包
func (p *Session) SendPacket(message []byte) error {
	p.RLock()
	defer p.RUnlock()

	return p.conn.WriteMessage(p.messageType, message)
}

// SessionId
// 获取 SessionId 数据
func (p *Session) SessionId() string {
	return p.sessionId
}

// Close
// 关闭服务
func (p *Session) Close() {
	//	释放引用计数
	p.quoteTotal--

	//	释放协程
	p.offline <- struct{}{}
}

// Replace
// 替换连接 —— 当有一个同样的新连接进来时
// 应当接受新的连接，而不应该使用旧连接
// 当使用新连接时，Session 中的状态与数据都应该被复用
// 所以替换连接时，只是换了一个连接的源，它不会清除当前玩家的状态数据
// 应用场景： 当用户刷新前端页面时，再次用同一个玩家ID进来，即可继续游戏
// 而不是退出游戏重新开始
func (p *Session) Replace(conn *websocket.Conn) error {
	p.Lock()
	defer p.Unlock()

	//	替换标识，当标识为 True 时
	// 	所有接收到的数据包都会被丢弃
	p.replacing = true

	//	仅关闭旧的连接
	if err := p.conn.Close(); err != nil {
		return err
	}

	//	当连接关闭成功时应当使用新的连接
	p.conn = conn
	p.replacing = false

	//	计一次引用
	p.quoteTotal++

	return nil
}

// Wait
// 等待客户端断开
func (p *Session) Wait() {
	<-p.offline
}

// SetConnection
// 设置连接 —— 此方法会覆盖当前连接
func (p *Session) SetConnection(conn *websocket.Conn) {
	p.Lock()
	defer p.Unlock()

	p.conn = conn
}

// QuoteTotal
// 获取客户端的引用计数器
func (p *Session) QuoteTotal() int {
	return p.quoteTotal
}
